Skip to content

feature: events (LAN mini-seasons)#337

Open
Flegma wants to merge 11 commits into
mainfrom
feature/events
Open

feature: events (LAN mini-seasons)#337
Flegma wants to merge 11 commits into
mainfrom
feature/events

Conversation

@Flegma

@Flegma Flegma commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Implements the approved Events feature: an event is a curated container grouping assigned tournaments; leaderboards and standings are computed on read over the derived match set. Design doc: docs/plans/2026-07-03-events-feature-design.md (workspace docs), implementation plan: docs/plans/2026-07-04-events-feature-implementation-plan.md.

What's included

  • Migration 1867000000300_events: tables events, event_organizers, event_tournaments, event_teams, event_players, enum e_event_status (Setup/Live/Finished). Event deletion cascades membership rows only; matches/tournaments/players are never touched.
  • is_event_organizer computed-field function (clone of is_tournament_organizer).
  • v_event_player_stats view: clone of v_tournament_player_stats keyed by event, membership derived via event_tournaments -> tournament_stages -> tournament_brackets.
  • get_event_leaderboard(_event_id, _category, _match_type, _min_rounds default 10) returning leaderboard_entries; categories rating/adr/kdr/kills/wins; non-empty event_players acts as a roster filter (empty = everyone). Low _min_rounds default because the global 50-round floor would blank a one-day LAN.
  • Hasura metadata: relationships, is_organizer computed field, player_stats manual relationship, settings-gated insert via public.create_events_role (same _exists mechanism as tournaments, full role fan-out), guest select hides Setup events (including all child tables, the stats view, and the leaderboard function), owner-only delete and owner-only co-organizer management, function tracking.
  • No NestJS changes; no changes to matches, player_elo, or ELO calculation.

Post-review fixes, round 1 (2026-07-04)

An 8-dimension find + adversarial-verify review produced:

  • Setup events fully hidden from the public (security). Child tables, stats view, and leaderboard function previously read wide open, letting anonymous clients enumerate a not-yet-public event's roster and standings by id. All event-derived read paths now apply the events table's status _neq Setup guest gate plus an organizer-aware user branch; get_event_leaderboard returns empty for a Setup or unknown event. Commit 5b7a3bf.
  • Rollback safety. down.sql clears the boot-phase SQL digests (guarded by to_regclass) so a forward deploy after a rollback recreates the view/functions. Commit cd8299a.

Post-review fixes, round 2 (2026-07-07)

A second full-PR review (8 finder angles, per-candidate adversarial verify, then an adversarial regression pass on the fixes) produced:

  • Leaderboard pagination fixed. get_event_leaderboard hard-capped output at LIMIT 100 while the web paginates via Hasura-level order_by/limit/offset (like the global get_leaderboard, which has no cap), silently truncating events with more than 100 players; the cap is removed. An explicit NULL _min_rounds also silently emptied the board (HAVING >= NULL); it now means "no minimum". Commit 1f006a0.
  • create_events_role='moderator' no longer bricks event creation. The insert-check role fan-out skipped the moderator tier entirely (no moderator permission, and the higher roles' _in lists jumped from streamer to match_organizer), so setting the role to moderator would have denied creation to everyone below administrator. Fixed with a moderator permission block and corrected lists. The tournaments clone source (create_tournaments_role checks) has the same pre-existing gap; flagged as a follow-up rather than widened into this PR. Commit eed53d6.

Verification

Run against an ephemeral TimescaleDB + Hasura v2.49.2 stack with the repo's full migration chain, SQL dirs, metadata, and dev fixtures applied (metadata ic list consistent):

  • v_event_player_stats verified row-identical to v_tournament_player_stats for a single-tournament event (EXCEPT check, both directions), and union growth verified with a second tournament.
  • All five leaderboard categories exercised with multi-map aggregation, plus _min_rounds threshold and _match_type filter behavior.
  • Role-based GraphQL audit: guest cannot see Setup events; organizer insert auto-sets organizer_steam_id; co-organizers can manage membership but cannot delete the event or manage the co-organizer list (two escalations found and fixed in c77cf4b).
  • Round-1 and round-2 fixes verified by adversarial re-review (permission-gate role matrix, plpgsql validity, YAML parse + role-list monotonicity, Hasura function pagination semantics). NOT yet re-applied against a live Hasura; the dev-stack steps below are the confirming pass.

Pre-merge checklist (needs the real dev stack)

  • yarn dev boot so HasuraService.setup() hash-applies the new/edited SQL files, then hasura metadata apply + hasura metadata ic list to confirm no inconsistencies.
  • Re-run the view equivalence check and leaderboard smoke test against real-scale data.
  • Guest query for a Setup event's event_tournaments/v_event_player_stats/get_event_leaderboard returns zero rows; the organizer still sees them.
  • Leaderboard pagination past row 100 works on a large event (or synthetic data).

Product notes for review

  • Setup-event visibility (resolved): guests can no longer enumerate Setup events' membership/stats/leaderboard. This intentionally diverges from the shipped tournaments family, which still reads its child tables wide open; the same tightening there is a suggested follow-up ticket.
  • Organizer preview during Setup: get_event_leaderboard takes no session argument, so it returns empty for a Setup event even to its organizer. This affects the web detail page's Leaderboard tab AND its Teams and Players tab (whose participant fallback uses the same function when no players are explicitly attached); explicitly attached players/teams still show. Raw per-player stats remain visible to the organizer via v_event_player_stats. If organizer preview during Setup is wanted, make the function session-aware (add hasura_session json + is_event_organizer check) as a follow-up.
  • tournament_organizer role can manage/delete any event (admin-tier semantics, same as is_event_organizer's role bypass).
  • events.organizer_steam_id FK has no ON DELETE action, so deleting a player who organizes an event is blocked (deliberate; no player-deletion flow exists today).
  • Accepted review observations (consistent with platform conventions, follow-up candidates): v_event_player_stats derives deaths from player_kills (like its tournament clone) while get_event_leaderboard uses player_match_map_stats, so KDR can differ slightly between the two read paths; the event match-set CTE is duplicated between the view and the function (kept separate to preserve the verified NOT MATERIALIZED inlining); the wins CTE is computed for all categories; no ORDER BY tiebreaker at page boundaries (same as the global leaderboard).

Web PR follows (page family + zeus codegen); merge this first.

Flegma added 7 commits July 4, 2026 01:00
is_event_organizer() returns true for the owner, any co-organizer, or
admin-tier roles. Two permission blocks used it where owner-only
semantics were required, letting co-organizers delete events and
add/remove other co-organizers.

- public_events.yaml: delete_permissions for role user now filters on
  organizer_steam_id instead of is_organizer.
- public_event_organizers.yaml: insert/delete checks for role user now
  require event.organizer_steam_id to match the caller, and an explicit
  tournament_organizer block (check/filter: {}) is added so admin-tier
  co-organizer management is preserved after narrowing the user role.
…tion

get_event_leaderboard was LANGUAGE sql, so its body was parsed at CREATE
time against v_player_match_map_hltv, a view applied in a later boot
phase. Fresh installs only survived via a fragile session-level
check_function_bodies=false. Converted it to LANGUAGE plpgsql, whose
body is not parsed for relation references at creation time, and added
a RAISE on an unrecognized category to match get_leaderboard's behavior
instead of silently returning zeroed rows.

The events migration's down.sql dropped the event tables directly, but
is_event_organizer (which takes public.events as an argument) and
v_event_player_stats (a view over event_tournaments) are created in
later boot phases and are not reverted by re-running migrations, so
down.sql failed with dependency errors. Prepended drops for
get_event_leaderboard, v_event_player_stats and is_event_organizer
before the table drops, in dependency order.
…te pushdown

The e_matches CTE in v_event_player_stats is referenced 3 times (by
kd_agg, assists_agg, matches_agg), so PostgreSQL 12+ materializes it by
default. Hasura always queries this view per event, but the
materialized CTE builds the match set for every event on the instance
before the outer event_id filter discards all but one, so cost scales
with total events rather than the target event.

Mark the CTE NOT MATERIALIZED so the planner inlines it and pushes the
event_id filter down into the event_tournaments scan. This is a
planner hint only; verified result-equivalence and EXPLAIN pushdown
against the events-verify-db fixtures before committing.
Flegma added 4 commits July 4, 2026 19:56
The events table hides Setup-status events from the guest role
(filter status _neq Setup), but the child membership tables
(event_tournaments/teams/players/organizers), the v_event_player_stats
view, and the get_event_leaderboard function all read wide open, so an
anonymous client could enumerate a not-yet-public event's roster,
tournament list, co-organizers, and computed standings by supplying its
id directly. Because events aggregate over pre-existing/finished
tournaments, those reads return real populated data before the event
goes Live.

Gate every event-derived read path the same way the events table does:
- guest select on the four child tables and the stats view now filters
  on event.status _neq Setup.
- a user-role select (inherited by every authenticated role) adds the
  organizer branch so an organizer still sees their own Setup event's
  children and stats, matching the events table _or [is_organizer,
  status _neq Setup] pattern.
- get_event_leaderboard returns an empty set for a Setup or unknown
  event instead of computing standings for it.
down.sql dropped the boot-phase view/functions but left their stored
digests in migration_hashes.hashes. Because the boot loader skips
re-creating an object whose digest is unchanged, a forward deploy after
a rollback would recreate the tables but never recreate
v_event_player_stats / get_event_leaderboard / is_event_organizer,
leaving the events feature silently broken. Delete the three digests in
down.sql, guarded by to_regclass so it is a no-op if migration_hashes
does not exist yet.
get_event_leaderboard hard-capped its output at LIMIT 100 while the web
paginates the tracked function with Hasura-level order_by/limit/offset
(the same pattern as the global get_leaderboard, which has no cap), so
an event with more than 100 qualifying players would silently truncate
both the pages and the aggregate count. Drop the cap.

Also coalesce an explicit NULL _min_rounds to 0: HAVING SUM(...) >= NULL
filters every row, so a null argument silently emptied the board instead
of meaning "no minimum".
The insert checks' role fan-out skipped the moderator tier: no moderator
insert permission existed and the match_organizer/tournament_organizer
_in lists jumped from streamer to match_organizer. Setting
public.create_events_role to 'moderator' would therefore deny event
creation to every role below administrator, including moderators
themselves. Add the moderator permission block and slot moderator into
the higher roles' lists so each list covers every role at or below its
own tier.

The tournaments clone source (create_tournaments_role checks in
public_tournaments.yaml) has the same gap; flagged as a follow-up
rather than widened into this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant